Terraformが依存関係を理解できるようにコードを書こう
問題
S3バケットを作成するTerraformのコードを書きました。命名のためにlocal変数を使っています。
locals { bucket_name = "kazue-hogehoge" } resource "aws_s3_bucket" "sample" { bucket = local.bucket_name }
このバケットに、さらにバージョニングの設定を足したいと思います。ここで問題です。以下2つのコード、どちらがより良いコードと言えるでしょうか?
A: bucket attributeの参照元としてlocal変数を使う
resource "aws_s3_bucket_versioning" "sample" { bucket = local.bucket_name versioning_configuration { status = "Enabled" } }
B: bucket attributeの参照元としてaws_s3_bucketのattributeを使う
resource "aws_s3_bucket_versioning" "sample" { bucket = aws_s3_bucket.sample.bucket versioning_configuration { status = "Enabled" } }
私は、「B: bucket attributeの参照元としてaws_s3_bucketのattributeを使う」の方がより良いコードだと考えます。理由は、エラーが起きにくいからです。
解説
aws_s3_bucket_versioning
リソースは、設定対象となるS3バケットに依存しています。当然ですね。バケットの設定項目の一つですから。
ですが、Aのコードだとその依存関係をTerraformは理解していません。故に、「バケットをプロビジョニングする前にバージョニングのプロビジョニングを行おうとしてエラーになる」というパターンが発生し得ます。
Aでターゲットを絞ってプロビジョニングしてみる
例を挙げます。さっきのAのコードで、-target
オプションを使って、aws_s3_bucket_versioning
リソースだけプロビジョニングしてみましょう。
プラン結果は以下です。当然 aws_s3_bucket_versioning
だけが作成予定です。
% terraform plan -target="aws_s3_bucket_versioning.sample" Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # aws_s3_bucket_versioning.sample will be created + resource "aws_s3_bucket_versioning" "sample" { + bucket = "kazue-hogehoge" + id = (known after apply) + versioning_configuration { + mfa_delete = (known after apply) + status = "Enabled" } } Plan: 1 to add, 0 to change, 0 to destroy. (略)
applyするとエラーになります。そりゃバケットがないのでそうですよね。
% terraform apply -target="aws_s3_bucket_versioning.sample" -auto-approve (略) │ Error: error creating S3 bucket versioning for kazue-hogehoge: NoSuchBucket: The specified bucket does not exist │ status code: 404, request id: MA1QPNDWZSAP0PZX, host id: WCEsy4o7hf4w7i8vJSuQ6QmFvCGFIvz0r8tCvlJ7ej7qubpkNvPyCWI1PaMUxmNMZ2icb4sneqM= │ │ with aws_s3_bucket_versioning.sample, │ on main.tf line 25, in resource "aws_s3_bucket_versioning" "sample": │ 25: resource "aws_s3_bucket_versioning" "sample" { │
Bでターゲットを絞ってプロビジョニングしてみる
では、同じことをBのコードでもやってみましょう。
% terraform plan -target="aws_s3_bucket_versioning.sample" Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # aws_s3_bucket.sample will be created + resource "aws_s3_bucket" "sample" { + acceleration_status = (known after apply) + acl = (known after apply) + arn = (known after apply) + bucket = "kazue-hogehoge" + bucket_domain_name = (known after apply) + bucket_regional_domain_name = (known after apply) + force_destroy = false + hosted_zone_id = (known after apply) + id = (known after apply) + object_lock_enabled = (known after apply) + policy = (known after apply) + region = (known after apply) + request_payer = (known after apply) + tags_all = (known after apply) + website_domain = (known after apply) + website_endpoint = (known after apply) + cors_rule { + allowed_headers = (known after apply) + allowed_methods = (known after apply) + allowed_origins = (known after apply) + expose_headers = (known after apply) + max_age_seconds = (known after apply) } + grant { + id = (known after apply) + permissions = (known after apply) + type = (known after apply) + uri = (known after apply) } + lifecycle_rule { + abort_incomplete_multipart_upload_days = (known after apply) + enabled = (known after apply) + id = (known after apply) + prefix = (known after apply) + tags = (known after apply) + expiration { + date = (known after apply) + days = (known after apply) + expired_object_delete_marker = (known after apply) } + noncurrent_version_expiration { + days = (known after apply) } + noncurrent_version_transition { + days = (known after apply) + storage_class = (known after apply) } + transition { + date = (known after apply) + days = (known after apply) + storage_class = (known after apply) } } + logging { + target_bucket = (known after apply) + target_prefix = (known after apply) } + object_lock_configuration { + object_lock_enabled = (known after apply) + rule { + default_retention { + days = (known after apply) + mode = (known after apply) + years = (known after apply) } } } + replication_configuration { + role = (known after apply) + rules { + delete_marker_replication_status = (known after apply) + id = (known after apply) + prefix = (known after apply) + priority = (known after apply) + status = (known after apply) + destination { + account_id = (known after apply) + bucket = (known after apply) + replica_kms_key_id = (known after apply) + storage_class = (known after apply) + access_control_translation { + owner = (known after apply) } + metrics { + minutes = (known after apply) + status = (known after apply) } + replication_time { + minutes = (known after apply) + status = (known after apply) } } + filter { + prefix = (known after apply) + tags = (known after apply) } + source_selection_criteria { + sse_kms_encrypted_objects { + enabled = (known after apply) } } } } + server_side_encryption_configuration { + rule { + bucket_key_enabled = (known after apply) + apply_server_side_encryption_by_default { + kms_master_key_id = (known after apply) + sse_algorithm = (known after apply) } } } + versioning { + enabled = (known after apply) + mfa_delete = (known after apply) } + website { + error_document = (known after apply) + index_document = (known after apply) + redirect_all_requests_to = (known after apply) + routing_rules = (known after apply) } } # aws_s3_bucket_versioning.sample will be created + resource "aws_s3_bucket_versioning" "sample" { + bucket = "kazue-hogehoge" + id = (known after apply) + versioning_configuration { + mfa_delete = (known after apply) + status = "Enabled" } } Plan: 2 to add, 0 to change, 0 to destroy. (略)
aws_s3_bucket
もplanに含まれましたね。(attribute多すぎでとても縦長…)
これは-target
オプションは、そこに指定されたリソース(今回だとaws_s3_bucket_versioning
)に加えて、そのリソースが依存している、つまりそのリソースを作るのに必要になる別のリソース(今回だとaws_s3_bucket
)も併せてプロビジョニングするからです。aws_s3_bucket_versioning.sample
のbucket
arrtibute値にaws_s3_bucket.sample
のattributeを使ったのでこういう結果になりました。この後applyも成功しました。applyのログを見ていると以下のようになっており、バケットの作成完了を待ってからバージョニングの設定が始まっていることがわかります。
aws_s3_bucket.sample: Creating... aws_s3_bucket.sample: Creation complete after 6s [id=kazue-hogehoge] aws_s3_bucket_versioning.sample: Creating... aws_s3_bucket_versioning.sample: Creation complete after 3s [id=kazue-hogehoge]
でも-target
オプションなんてそうそう使わへんやん
「-target
なんてほとんど使わないオプション持ち出して、『こっちの方が良いコードです(`・ω・´)キリッ』なんて言われてもねぇ…」とシラけたあなた。おっしゃるとおりです。私もそう思います。
ですが、-target
オプションを使わずとも、先に挙げたような「バケットをプロビジョニングする前にバージョニングのプロビジョニングを行おうとしてエラーになる」ケースは結構発生すると考えています。依存関係をTerraformが理解していない以上、どちらが先にプロビジョニングされるかは、完全にTerraform任せになります。
- 通常私達はTerraformで複数リソースを同時並行的にプロビジョニングします。デフォルト並列数は10です。依存関係にあるリソースが、同時に作成開始するケースが発生します。それどころか、他のリソースももっと大量にプロビジョニングするコードである場合は、他のリソースの作成に紛れて依存しているリソース(バージョニング)→依存されている(バケット)の順番で実行される場合もあります。
- 今回のS3バケットのような、すぐに作成されるリソースの場合、たとえそれに依存しているリソース(バージョニング)と同時に作成開始したとしてもエラーになる確率は低いでしょう。(実際上記Aのコードも
-target
を外せばapplyは正常完了しました。) ですが、もっと作成に時間がかかるリソース、例えばEKSクラスター等の場合はより注意が必要です。
おまけ: グラフで確認
terraform graph
コマンドでAとBのコードのリソースの依存関係を図示してみました。
% terraform graph | dot -Tpng > graph.png
Aのグラフ
aws_s3_bucket
と aws_s3_bucket_versioning
に依存関係はありませんね。
Bのグラフ
aws_s3_bucket_versioning
がaws_s3_bucket
に依存しています。
まとめ
以上、リソース間の依存関係をTerraformが理解できるように書きましょう、という話でした。細かい話ですが、これが原因でエラーが出たとき問題特定が辛かったなぁと思いだして書きました(S3バケットではなかったですが)。日々applyしているような環境ではあまり起きないんですが、久しぶりに別環境を一から作成する時に発生しがちです。お気をつけください。また、明示的に依存関係が表せない場合は、depends_on
の使用も検討しましょう。